package com.ljw.security.distributedsecurityuaa.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import javax.sql.DataSource;
import java.util.Arrays;

/**
 * @FileName AuthorizationServer
 * @Description TODO 配置OAuth2.0 认证授权 服务器。
 * 认证授权服务配置总结：认证授权服务配置分成三大块，可以关联记忆。
 *  1.既然要完成认证，它首先得知道客户端信息从哪儿读取，因此要进行客户端详情配置。
 *      客户端详情存储方式：内存 和 数据库
 *  2. 既然要颁发token，那必须得定义token的相关暴露endpoint，以及token如何存取，以及客户端支持哪些类型的 token。
 *      token管理服务（token存储策略） 和  配置（暴露出）令牌访问端点的url
 *      重点：
 *          （1）令牌token存储策略：InMemoryTokenStore，JdbcTokenStore，JwtTokenStore，RedisTokenStore
 *          （2）JWT令牌优势
 *          （3）授权码模式的授权码如何 存取？内存方式 和  数据库存储方式
 *  3. 既然暴露除了一些endpoint，那对这些endpoint可以定义一些安全上的约束等。
 *
 *  OAuth2.0 的  向认证授权服务器申请令牌的  4中模式：
 *  1. 授权码模式。
 *          1.1 向认证服务器请求授权码：response_type=code：
 *              /oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com
 *          1.2 使用授权码模式向认证服务器请求令牌：grant_type：授权类型，填写authorization_code，表示授权码模式
 *              /oauth/token? client_id=c1&client_secret=secret&grant_type=authorization_code&code=5PgfcD&redirect_uri=http://w ww.baidu.com
 *          这种模式是四种模式中最安全的一种模式。一般用于client是Web服务器端应用或第三方的原生App调用资源服务 的时候。
 *          因为在这种模式中access_token不会经过浏览器或移动端的App，而是直接从服务端去交换，这样就最大 限度的减小了令牌泄漏的风险。
 *  2. 简化模式。 参数描述同授权码模式 ，注意response_type=token，说明是简化模式
 *          /oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com
 *          跳转的URL携带token： http://aa.bb.cc/receive#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZW5hbn...
 *          一般来说，简化模式用于没有服务器端的第三方单页面应用，因为没有服务器端就无法接收授权码。
 *  3. 密码模式。 grant_type=password
 *          /oauth/token? client_id=c1&client_secret=secret&grant_type=password&username=shangsan&password=123
 *          这种模式十分简单，但是却意味着直接将用户敏感信息泄漏给了client，因此这就说明这种模式只能用于client是我 们自己开发的情况下。
 *          因此密码模式一般用于我们自己开发的，第一方原生App或第一方单页面应用。
 *  4. 客户端模式。
 *          /oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials
 *          这种模式是最方便但最不安全的模式。因此这就要求我们对client完全的信任，而client本身也是安全的。
 *          因 此这种模式一般用来提供给我们完全信任的服务器端服务。比如，合作方系统对接，拉取一组用户信息。
 *
 * JWT令牌：
 * //普通令牌解析服务，问题：
    //         当资源服务和授权服务不在一起时,资源服务使用RemoteTokenServices 远程请求授权 服务验证token，如果访问量较大将会影响系统的性能 。
    //解决上边问题：
    //          令牌采用JWT格式即可解决上边的问题，用户认证通过会得到一个JWT令牌，JWT令牌中已经包括了用户相关的信 息，
    //          客户端只需要携带JWT访问资源服务，资源服务根据事先约定的算法自行完成令牌校验，无需每次都请求认证 服务完成授权。
    //1、什么是JWT？
    //      JSON Web Token（JWT）是一个开放的行业标准（RFC 7519），它定义了一种简介的、自包含的协议格式，
    //      用于 在通信双方传递json对象，传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公 钥/私钥对来签名，防止被篡改。
    //      JWT令牌由三部分组成，每部分中间使用点（.）分隔，比如：xxxxx.yyyyy.zzzzz
    //      base64UrlEncode(header)：jwt令牌的第一部分。
    //      base64UrlEncode(payload)：jwt令牌的第二部分。
    //      secret：签名所使用的密钥。即SIGNING_KEY
       2. JWT令牌的优点：
        1）jwt基于json，非常方便解析。
        2）可以在令牌中自定义丰富的内容，易扩展。
        3）通过非对称加密算法及数字签名技术，JWT防止篡改，安全性高。
        4）资源服务使用JWT可不依赖认证服务即可完成授权。

        缺点： １）JWT令牌较长，占存储空间比较大。

 * @Author ljw
 * @Date 2020/11/22 20:10
 * @Version 1.0
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    PasswordEncoder passwordEncoder;




    /**
     * 1. 配置客户端详细信息
     * @param clients
     * @throws Exception
     */
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        /*1. ClientDetailsServiceConfigurer 能够使用内存或者JDBC来存储客户端详情服务（ClientDetailsService），
        *
        * */
        //1.1 使用数据库来存储客户端详情
        clients.withClientDetails(clientDetailsService);

        //1.2 使用内存来存储客户端详情
        /*clients.inMemory()// 使用in‐memory存储
                .withClient("c1")//client_id
                .secret(new BCryptPasswordEncoder().encode("secret"))//客户端秘钥
                .resourceIds("r1")//资源id
                .authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")// 该client允许的授权类型
                .scopes("all")// 允许的授权范围
                .autoApprove(false)//false：跳转到授权页面；true：不用跳转到授权页面，直接发令牌
                .redirectUris("http://www.baidu.com");//加上验证回调地址*/
    }

    @Bean
    public ClientDetailsService clientDetailsService(DataSource dataSource) {
        ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
        ((JdbcClientDetailsService) clientDetailsService).setPasswordEncoder(passwordEncoder);
        return clientDetailsService;
    }




    @Autowired
    private TokenStore tokenStore;


    //注入JWT令牌实例
    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;

    /**
     * 2.1 令牌管理服务
     * @return
     */
    @Bean
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service=new DefaultTokenServices();
        service.setClientDetailsService(clientDetailsService);//客户端详细信息
        service.setSupportRefreshToken(true);//是否产生刷新令牌
        service.setTokenStore(tokenStore);//令牌token存储策略

        //令牌增强，使用JWT令牌，添加下面3行
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
        service.setTokenEnhancer(tokenEnhancerChain);

        service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
        service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
        return service;
    }

    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;

    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * 2.2 配置（暴露出）令牌访问端点的url
     * AuthorizationServerEndpointsConfigurer 这个配置对象有一个叫做 pathMapping() 的方法用来配置端点URL链 接，
     * 它有两个参数：
     *          第一个参数：String 类型的，这个端点URL的默认链接。
     *          第二个参数：String 类型的，你要进行替代的URL链接。
     *    以上的参数都是以 "/" 字符为开始的字符串，框架的默认URL链接如下列表，可以作为这个 pathMapping() 方法的 第一个参数：
     *              /oauth/authorize：授权端点。
     *              /oauth/token：令牌端点。
     *              /oauth/confirm_access：用户确认授权提交端点。
     *              /oauth/error：授权服务错误信息端点。
     *              /oauth/check_token：用于资源服务访问的令牌解析端点。
     *              /oauth/token_key：提供公有密匙的端点，如果你使用JWT令牌的话。
     *              需要注意的是授权端点这个URL应该被Spring Security保护起来只供授权用户访问.
     * @param endpoints
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{
        endpoints.authenticationManager(authenticationManager)//密码模式需要
                .authorizationCodeServices(authorizationCodeServices)//授权码模式需要
                .tokenServices(tokenService())//令牌管理服务
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);//允许post提交访问令牌
    }


    /*
    * 授权码模式的授权码如何 存取？内存方式 和  数据库存储方式
    * */
   /* @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        //设置授权码模式的授权码如何 存取，暂时采用内存方式
        return new InMemoryAuthorizationCodeServices();
    }*/

    @Bean
    public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
        //设置授权码模式的授权码如何存取，数据库存储
        return new JdbcAuthorizationCodeServices(dataSource);
    }


    /**
     * 3. 配置令牌端点(Token Endpoint)的安全约束
     * @param security
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception{
        security .tokenKeyAccess("permitAll()")   //  /oauth/token：获取令牌的端点。permitAll()：公开
                .checkTokenAccess("permitAll()")  //   /oauth/check_token：访问资源服务时，需要用的的令牌解析端点。"permitAll()"：该端点公开
                .allowFormAuthenticationForClients(); //  允许表单认证
    }
}
